{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# PydanticOutputParser\n",
    "\n",
    "`PydanticOutputParser`는 언어 모델의 출력을 **구조화된 정보**로 변환하는 데 도움을 주는 클래스입니다. 이 클래스는 단순 텍스트 응답 대신 **명확하고 체계적인 형태로 필요한 정보를 제공**할 수 있습니다.\n",
    "\n",
    "이 클래스를 활용하면 언어 모델의 출력을 특정 데이터 모델에 맞게 변환하여 정보를 더 쉽게 처리하고 활용할 수 있습니다.\n",
    "\n",
    "## 주요 메서드\n",
    "\n",
    "`PydanticOutputParser` (대부분의 OutputParser에 해당)에는 주로 **두 가지 핵심 메서드**가 구현되어야 합니다.\n",
    "\n",
    "- **`get_format_instructions()`**: 언어 모델이 출력해야 할 정보의 형식을 정의하는 지침을 제공합니다. 예를 들어, 언어 모델이 출력해야 할 데이터의 필드와 그 형태를 설명하는 지침을 문자열로 반환할 수 있습니다. 이 지침은 언어 모델이 출력을 구조화하고 특정 데이터 모델에 맞게 변환하는 데 매우 중요합니다.\n",
    "- **`parse()`**: 언어 모델의 출력(문자열로 가정)을 받아 이를 특정 구조로 분석하고 변환합니다. Pydantic과 같은 도구를 사용하여 입력된 문자열을 사전 정의된 스키마에 따라 검증하고, 해당 스키마를 따르는 데이터 구조로 변환합니다.\n",
    "\n",
    "## 참고 자료\n",
    "\n",
    "- [Pydantic 공식 도큐먼트](https://docs.pydantic.dev/latest/)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from dotenv import load_dotenv\n",
    "\n",
    "load_dotenv()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n",
    "# !pip install langchain-teddynote\n",
    "from langchain_teddynote import logging\n",
    "\n",
    "# 프로젝트 이름을 입력합니다.\n",
    "logging.langsmith(\"CH03-OutputParser\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 실시간 출력을 위한 import\n",
    "from langchain_teddynote.messages import stream_response"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_openai import ChatOpenAI\n",
    "from langchain_core.output_parsers import PydanticOutputParser\n",
    "from pydantic import BaseModel, Field\n",
    "\n",
    "\n",
    "llm = ChatOpenAI(temperature=0, model_name=\"gpt-4.1-mini\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "다음은 이메일 본문 예시입니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "email_conversation = \"\"\"From: 김철수 (chulsoo.kim@bikecorporation.me)\n",
    "To: 이은채 (eunchae@teddyinternational.me)\n",
    "Subject: \"ZENESIS\" 자전거 유통 협력 및 미팅 일정 제안\n",
    "\n",
    "안녕하세요, 이은채 대리님,\n",
    "\n",
    "저는 바이크코퍼레이션의 김철수 상무입니다. 최근 보도자료를 통해 귀사의 신규 자전거 \"ZENESIS\"에 대해 알게 되었습니다. 바이크코퍼레이션은 자전거 제조 및 유통 분야에서 혁신과 품질을 선도하는 기업으로, 이 분야에서의 장기적인 경험과 전문성을 가지고 있습니다.\n",
    "\n",
    "ZENESIS 모델에 대한 상세한 브로슈어를 요청드립니다. 특히 기술 사양, 배터리 성능, 그리고 디자인 측면에 대한 정보가 필요합니다. 이를 통해 저희가 제안할 유통 전략과 마케팅 계획을 보다 구체화할 수 있을 것입니다.\n",
    "\n",
    "또한, 협력 가능성을 더 깊이 논의하기 위해 다음 주 화요일(1월 15일) 오전 10시에 미팅을 제안합니다. 귀사 사무실에서 만나 이야기를 나눌 수 있을까요?\n",
    "\n",
    "감사합니다.\n",
    "\n",
    "김철수\n",
    "상무이사\n",
    "바이크코퍼레이션\n",
    "\"\"\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "출력 파서를 사용하지 않는 경우 예시"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from itertools import chain\n",
    "from langchain_core.prompts import PromptTemplate\n",
    "\n",
    "prompt = PromptTemplate.from_template(\n",
    "    \"다음의 이메일 내용중 중요한 내용을 추출해 주세요.\\n\\n{email_conversation}\"\n",
    ")\n",
    "\n",
    "llm = ChatOpenAI(temperature=0, model_name=\"gpt-4.1-mini\")\n",
    "\n",
    "chain = prompt | llm\n",
    "\n",
    "answer = chain.stream({\"email_conversation\": email_conversation})\n",
    "\n",
    "output = stream_response(answer, return_output=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(output)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "위와 같은 이메일 내용이 주어졌을 때 아래의 Pydantic 스타일로 정의된 클래스를 사용하여 이메일의 정보를 파싱해 보겠습니다.\n",
    "\n",
    "참고로, Field 안에 `description` 은 텍스트 형태의 답변에서 주요 정보를 추출하기 위한 설명입니다. LLM 이 바로 이 설명을 보고 필요한 정보를 추출하게 됩니다. 그러므로 이 설명은 정확하고 명확해야 합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [],
   "source": [
    "class EmailSummary(BaseModel):\n",
    "    person: str = Field(description=\"메일을 보낸 사람\")\n",
    "    email: str = Field(description=\"메일을 보낸 사람의 이메일 주소\")\n",
    "    subject: str = Field(description=\"메일 제목\")\n",
    "    summary: str = Field(description=\"메일 본문을 요약한 텍스트\")\n",
    "    date: str = Field(description=\"메일 본문에 언급된 미팅 날짜와 시간\")\n",
    "\n",
    "\n",
    "# PydanticOutputParser 생성\n",
    "parser = PydanticOutputParser(pydantic_object=EmailSummary)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# instruction 을 출력합니다.\n",
    "print(parser.get_format_instructions())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "프롬프트를 정의합니다.\n",
    "\n",
    "1. `question`: 유저의 질문을 받습니다.\n",
    "2. `email_conversation`: 이메일 본문의 내용을 입력합니다.\n",
    "3. `format`: 형식을 지정합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "prompt = PromptTemplate.from_template(\n",
    "    \"\"\"\n",
    "You are a helpful assistant. Please answer the following questions in KOREAN.\n",
    "\n",
    "QUESTION:\n",
    "{question}\n",
    "\n",
    "EMAIL CONVERSATION:\n",
    "{email_conversation}\n",
    "\n",
    "FORMAT:\n",
    "{format}\n",
    "\"\"\"\n",
    ")\n",
    "\n",
    "# format 에 PydanticOutputParser의 부분 포맷팅(partial) 추가\n",
    "prompt = prompt.partial(format=parser.get_format_instructions())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "다음으로는 Chain 을 생성합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "# chain 을 생성합니다.\n",
    "chain = prompt | llm"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "체인을 실행하고 결과를 확인합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# chain 을 실행하고 결과를 출력합니다.\n",
    "response = chain.stream(\n",
    "    {\n",
    "        \"email_conversation\": email_conversation,\n",
    "        \"question\": \"이메일 내용중 주요 내용을 추출해 주세요.\",\n",
    "    }\n",
    ")\n",
    "\n",
    "# 결과는 JSON 형태로 출력됩니다.\n",
    "output = stream_response(response, return_output=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "마지막으로 `parser`를 사용하여 결과를 파싱하고 `EmailSummary` 객체로 변환합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# PydanticOutputParser 를 사용하여 결과를 파싱합니다.\n",
    "structured_output = parser.parse(output)\n",
    "print(structured_output)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## parser 가 추가된 체인 생성\n",
    "\n",
    "출력 결과를 정의한 Pydantic 객체로 생성할 수 있습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 출력 파서를 추가하여 전체 체인을 재구성합니다.\n",
    "chain = prompt | llm | parser"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# chain 을 실행하고 결과를 출력합니다.\n",
    "response = chain.invoke(\n",
    "    {\n",
    "        \"email_conversation\": email_conversation,\n",
    "        \"question\": \"이메일 내용중 주요 내용을 추출해 주세요.\",\n",
    "    }\n",
    ")\n",
    "\n",
    "# 결과는 EmailSummary 객체 형태로 출력됩니다.\n",
    "response"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## with_structured_output()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`.with_structured_output(Pydantic)`을 사용하여 출력 파서를 추가하면, 출력을 Pydantic 객체로 변환할 수 있습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "llm_with_structered = ChatOpenAI(\n",
    "    temperature=0, model_name=\"gpt-4.1-mini\"\n",
    ").with_structured_output(EmailSummary)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# invoke() 함수를 호출하여 결과를 출력합니다.\n",
    "answer = llm_with_structered.invoke(email_conversation)\n",
    "answer"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**참고**\n",
    "\n",
    "한 가지 아쉬운 점은 `.with_structured_output()` 함수는 `stream()` 기능을 지원하지 않습니다."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "py-test",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
